<?php
declare(strict_types=1);

namespace Zlf\AppPaging;

use Exception;

/**
 * 数据分页组件
 * 提供了对数据进行分页处理的功能，包括获取总页数、当前页码、上一页/下一页等信息，并能生成分页链接。
 *
 * @property-read int $total 数据总条数;
 * @property-read int $size 数据分页大小;
 * @property-read int $pageCount 数据总页数;
 * @property-read int $page 当前页码;
 * @property-read int $pagesNum 页码个数;
 * @property-read ?int $prev 上一页页码;
 * @property-read ?int $next 下一页页码;
 * @property-read ?int $first 首页;
 * @property-read ?int $last 尾页;
 * @property-read int $start 开始条数;
 * @property-read int $end 结束条数;
 * @property-read int $limit 数据库分页limit;
 * @property-read int $offset 数据库分页offset;
 */
class Paging
{
    /**
     * 数据总条数
     * @var int
     */
    private $_total;

    /**
     * 分页大小
     * @var int 默认值为15
     */
    private $_size = 15;

    /**
     * 总页数
     * @var int
     */
    private $_pageCount;

    /**
     * 当前页码
     * @var int 默认值为1
     */
    private $_page = 1;

    /**
     * 页码数
     * @var int 默认值为8
     */
    private $_pagesNum = 8;

    /**
     * 分页初始化构造函数
     * 初始化分页类，设置总条数、每页显示数量及当前页码。
     *
     * @param int $total 数据总条数
     * @param int $pageSize 每页显示的数据条数，默认值为15
     * @param int $page 当前页码，默认值为1
     */
    public function __construct(int $total = 0, int $pageSize = 15, int $page = 1)
    {
        $this->_total = $total;
        $this->_size = $pageSize;
        $this->_pageCount = (int)ceil($this->_total / $this->_size); // 计算总页数
        $this->setPage($page ? $page : 1);
    }

    /**
     * 属性获取器
     * 根据属性名动态返回对应的值。
     *
     * @param string $name 属性名称
     * @return mixed 返回属性值
     * @throws Exception 如果访问未定义的属性则抛出异常
     */
    public function __get(string $name)
    {
        $method = 'get' . ucwords($name);
        if (in_array($method, get_class_methods($this))) {
            return call_user_func([$this, $method]);
        }
        $attr = get_object_vars($this);
        if (isset($attr["_$name"])) {
            if ($name === 'page') {
                return max(1, $attr["_$name"]);
            }
            return $attr["_$name"];
        }
        throw new Exception("未定义的属性 '$name'");
    }

    /**
     * 设置当前页
     * 设置并验证当前页码是否有效。
     *
     * @param int $_page 当前页码
     * @return $this 当前实例
     */
    public function setPage(int $_page): self
    {
        if ($_page < 1) {
            $this->_page = 1;
        } elseif ($_page > $this->_pageCount) {
            $this->_page = $this->_pageCount;
        } else {
            $this->_page = $_page;
        }
        return $this;
    }

    /**
     * 设置页码按钮数
     * 设置显示的页码按钮数。
     *
     * @param int $num 页码按钮数
     * @return $this 当前实例
     */
    public function setPagesNum(int $num): self
    {
        $this->_pagesNum = $num;
        return $this;
    }

    /**
     * 获取分页页码
     * 根据当前页码和其他参数生成需要显示的页码列表。
     *
     * @return array 返回页码列表
     */
    public function getPages(): array
    {
        $btnnum = min($this->_pagesNum, $this->pageCount);
        if ($this->_page === 1) {
            return range(1, $btnnum);
        }
        if ($this->_page === $this->_pageCount) {
            return range(max($this->_page - $this->_pagesNum + 1, 1), $this->_page);
        }
        $pageNum = $this->_pagesNum - 1;
        $half = (int)ceil(($this->_pagesNum - 1) / 2);
        $after = range($this->_page + 1, min($this->_page + $half, $this->_pageCount));
        $before = range(max($this->_page - $pageNum + count($after), 1), $this->_page - 1);
        $pages = array_merge($before, [$this->_page], $after);
        if (count($pages) < $this->_pagesNum) {
            if ($pages[0] === 1) {
                return range(1, min($this->pagesNum, $this->_pageCount));
            }
        }
        return $pages;
    }

    /**
     * 获取上一页页码
     * 如果有上一页，则返回上一页的页码；否则返回null。
     *
     * @return int|null
     */
    protected function getPrev()
    {
        if ($this->_page > 1) {
            return $this->_page - 1;
        }
        return null;
    }

    /**
     * 获取下一页页码
     * 如果有下一页，则返回下一页的页码；否则返回null。
     *
     * @return int|null
     */
    protected function getNext()
    {
        if ($this->_page < $this->_pageCount) {
            return $this->_page + 1;
        }
        return null;
    }

    /**
     * 获取首页页码
     * 如果存在页面，则返回首页页码1；否则返回null。
     *
     * @return int|null
     */
    protected function getFirst()
    {
        if ($this->_pageCount > 0) {
            return 1;
        }
        return null;
    }

    /**
     * 获取尾页页码
     * 返回尾页页码，如果不存在任何页面，则返回null。
     *
     * @return int|null
     */
    protected function getLast()
    {
        if ($this->_pageCount > 0) {
            return $this->_pageCount;
        }
        return null;
    }

    /**
     * 获取数据库查询LIMIT值
     * 返回每页显示的数据量。
     *
     * @return int
     */
    protected function getLimit()
    {
        return $this->_size;
    }

    /**
     * 获取数据库查询OFFSET值
     * 返回当前页的起始位置。
     *
     * @return int
     */
    protected function getOffset()
    {
        return ($this->_page - 1) * $this->_size;
    }

    /**
     * 数据开始的位置
     * 返回当前页第一条数据在总数据中的位置。
     *
     * @return int
     */
    private function getStart(): int
    {
        return ($this->_page - 1) * $this->_size + 1;
    }

    /**
     * 获取数据结尾
     * 返回当前页最后一条数据在总数据中的位置。
     *
     * @return int
     */
    private function getEnd(): int
    {
        return min($this->_page * $this->_size, $this->_total);
    }

    /**
     * 生成页码链接
     * 根据提供的基础URL和页码生成完整的分页链接。
     *
     * @param string $baseUrl 基础URL地址
     * @param int|null $page 分页页码
     * @param string $pageName 分页参数名，默认为'page'
     * @return string 完整的分页链接
     */
    public function generatePageLink($baseUrl, $page = null, $pageName = 'page'): string
    {
        if (!$page) {
            return '';
        }
        $baseUrl = urldecode($baseUrl);
        if (!$baseUrl) return "?" . http_build_query([$pageName => $page]);
        $parse = parse_url($baseUrl);
        $query = [];
        if (isset($parse['query']) && $parse['query']) {
            foreach (explode('&', $parse['query']) as $queryItem) {
                $param = explode("=", $queryItem);
                if (count($param) === 2 && strlen($param[0]) > 0 && strlen($param[1]) > 0) {
                    $query[$param[0]] = $param[1];
                }
            }
        }
        if ($page && $page > 0) {
            $query[$pageName] = $page;
        } else {
            unset($query[$pageName]);
        }
        $url = '';
        if (isset($parse['scheme']) && $parse['scheme']) $url .= $parse['scheme'] . '://';
        if (isset($parse['host']) && $parse['host']) $url .= $parse['host'];
        if (isset($parse['port']) && $parse['port']) $url .= ':' . $parse['port'];
        if (isset($parse['path']) && $parse['path']) $url .= $parse['path'];
        $url .= '?' . http_build_query($query);
        if (isset($parse['fragment']) && $parse['fragment']) $url .= "#" . $parse['fragment'];
        return $url;
    }

    /**
     * 返回分页信息
     * 返回包含分页详细信息的数组。
     *
     * @return array 分页信息数组
     */
    public function pages(): array
    {
        return [
            'total' => $this->_total, // 总条数
            'pageCount' => $this->_pageCount, // 总页数
            'start' => $this->start, // 起始条数
            'end' => $this->end, // 结束条数
            'size' => $this->_size, // 分页大小
            'page' => $this->_page, // 当前页
            'prev' => $this->prev, // 上一页
            'next' => $this->next, // 下一页
            'first' => $this->first, // 首页
            'last' => $this->last, // 尾页
            'pages' => $this->getPages(), // 页码列表
        ];
    }

    /**
     * 获取分页链接
     * 根据基础URL生成所有分页链接。
     *
     * @param string $baseUrl 基础URL
     * @param string $pageName 分页参数名，默认为'page'
     * @return array 包含所有分页链接的数组
     */
    public function links(string $baseUrl, string $pageName = 'page'): array
    {
        $pages = $this->pages();
        $pages['page'] = $this->generatePageLink($baseUrl, $pages['page'], $pageName);
        $pages['prev'] = $this->generatePageLink($baseUrl, $pages['prev'], $pageName);
        $pages['next'] = $this->generatePageLink($baseUrl, $pages['next'], $pageName);
        $pages['first'] = $this->generatePageLink($baseUrl, $pages['first'], $pageName);
        $pages['last'] = $this->generatePageLink($baseUrl, $pages['last'], $pageName);
        foreach ($pages['pages'] as $index => $item) {
            $pages['pages'][$index] = [
                'url' => $this->generatePageLink($baseUrl, $item, $pageName),
                'page' => $item,
                'active' => $this->page === $item
            ];
        }
        return $pages;
    }
}